package dpp;

// TODO zkontrolovat zmeny v public a hlavne okomentovat 

import java.util.EmptyStackException;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Stack;

import dpp.Token.TokenFlag;
import dpp.Token.TokenType;

/** Zajistuje spravne odsazovani. */
public class Indent {

	private static final String STRING_END = "end";
	private static final String STRING_UNTIL = "until";
	private static final String STRING_NEWLINE = "\n";
	
	private static final String SPACE = " ";
	
	/**
	 * Vrati <code>1</code> nebo <code>0</code> podle toho, jestli je dany druh 
	 * odsazeni jen realny nebo virtualni.
	 * 
	 * Vracena hodnota je ve volajici funkci pouzita jako koeficint, coz ma 
	 * efektivne za nasledek, ze se virtualni druh odsazovani fyzicky 
	 * neodsazuje.
	 *   
	 * @param type druh odsazeni
	 * @return <code>1</code> pokud je druh odsazeni realny;
	 *         <code>0</code> pokud je druh odsazeni virtualni
	 */
	private static int indentLevel(TokenType type) {
		
		return TokenType.VIRTUAL_ROUND_BRACKET.equals(type) ? 0 : 1;
	}
	
	/**
	 * Preskoci bile mezery a komentare, ktere nasleduji za <code>start</code>.
	 *
	 * @param startToken token, za kterym se preskakuji bile mezery a komentare
	 * , sam to nemusi byt bily komentar nebo mezera
	 * @return nejblizsi token za <code>start</code>, ktery neni bila mezera 
	 * ani komentar;
	 * <code>null</code> pokud takovy uz ve spojaku neni 
	 */
	private static Token skipWhitespaceCommentToken(Token startToken) {
		
		ISkipCondition tokenCondition = new ISkipCondition() {
			
			@Override
			public boolean condition(Token token) {

				boolean skippable = token.isType(TokenType.WHITESPACE) 
					|| token.isType(TokenType.COMMENT);
				
				return skippable;
			}
		};
		
		return skipTokenByCondition(startToken, tokenCondition);
	} 
	
	/**
	 * Preskoci vsechny tokeny od <code>token</code> (vcetne) a nemaji tridu 
	 * <code>klass</code> a text <code>text</code>. Porovnavani tridy tokenu je
	 * zde pro efektivitu, aby se nemusely porad porovnavat retezce. Misto toho
	 * se nejdrive porovnaji tridy a az kdyz jsou shodne, 
	 * porovnavaji se retezce.
	 * 
	 * @param token token, za kterym se preskakuji neodpovidajici tokeny
	 * @param type typ hledaneho tokenu
	 * @param text text hledaneho tokenu
	 * @return nejblizsi token za <code>token</code> (vcetne) se tridou 
	 * <code>klass</code> a textem <code>text</code>; <code>null</code> pokud 
	 * takovy uz ve spojaku neni 
	 */
	private static Token skipUntil(
		Token startToken, 
		final TokenType type, 
		final String text
	) {
		ISkipCondition tokenCondition = new ISkipCondition() {
			
			@Override
			public boolean condition(Token token) {
				// TODo remove
//				boolean skippable = token.type != type ||
//						!token.text.equalsIgnoreCase(text);
				
//				return skippable;
				boolean skippable = !(token.type == type 
					&& token.text.equalsIgnoreCase(text));
				
				return skippable;
			}
		};
		
		return skipTokenByCondition(startToken, tokenCondition);
	}

	
	private static Token skipUntilEndLineToken(Token startToken) {
	    	
		ISkipCondition tokenCondition = new ISkipCondition() {
			
			@Override
			public boolean condition(Token token) {
				
				boolean skippable = !token.containsFlag(TokenFlag.ENDS_LINE);
				return skippable;
			}
		};
		
		return skipTokenByCondition(startToken, tokenCondition);
	}
	
	/**
	 * Skips all tokens, which satisfies the conditions.
	 *  
	 * @param startToken
	 * @param tokenCondition
	 * @return null or token which not satisfies the conditions
	 */
	private static Token skipTokenByCondition(
		Token startToken, 
		ISkipCondition tokenCondition
	) {
		Token currentToken = startToken.nextToken;
		
		if (currentToken == null) {
			return null;
		}
		
		boolean skipCurrentToken = tokenCondition.condition(currentToken);
		while (currentToken != null && skipCurrentToken) {
			currentToken = currentToken.nextToken;
			skipCurrentToken = tokenCondition.condition(currentToken);
		}

		return currentToken;
	}

	/** 
	 * Zmeni hodnotu polozky <code>col</code> o <code>delta</code> u vsech 
	 * tokenu od <code>start</code> az do konce radku.
	 * 
	 * @param startToken prni token, kde se ma menit hodnota <code>col</code>
	 * @param delta o kolik se ma zmenit hodnota <code>col</code>
	 */
	static void changeColUntilEOL(Token startToken, int delta) {
		// TODO start isn't nice name

		final int currentRowNumber = startToken.row;
		ISkipCondition skipCondition = new ISkipCondition() {
			
			private int currentRow = currentRowNumber;

			@Override
			public boolean condition(Token token) {
				return token.row == currentRow;
			}
		};
		
		TokenConditionIterator TokensIterator = new TokenConditionIterator(
			startToken, 
			skipCondition
		);
		
		for (Token token : TokensIterator) {
			token.column += delta;
		}
	}

	/** 
	 * Zmeni hodnotu polozky <code>row</code> o <code>delta</code> u vsech 
	 * tokenu od <code>start</code> az do konce spojaku tokenu.
	 * 
	 * @param startToken prni token, kde se ma menit hodnota <code>row</code>
	 * @param indentationIncrease o kolik se ma zmenit hodnota <code>row</code>
	 */
//	 TODO use iterator?
	static void changeRowUntilEOF(Token startToken, int indentationIncrease) {
		
		Token currentToken = startToken;
		while (currentToken != null) {

			currentToken.row += indentationIncrease;
			currentToken = currentToken.nextToken;
		}
	}

	/**
	 * Zaridi, aby za danym tokenem byl prazdny radek.
	 *  
	 * @param token token, za kterym ma byt prazdna radka
	 */
	private static void ensureBlankLineAfter(Token token) {

		if (token.nextToken == null) {
			return;
		}

		token = skipUntilEndLineToken(token);

		// TODO move to method
		if (token == null || token.nextToken == null 
			|| token.nextToken.containsFlag(TokenFlag.ENDS_LINE)
		) { 
			return;
		}

		// create new token
		int tokenFlags = token.nextToken.getFlags();
		int row = token.nextToken.row;
		Token newToken = new Token(
			STRING_NEWLINE, // TODO create constant 
			TokenType.WHITESPACE, 
			tokenFlags,								
			row, 
			0, // TODO create constant
			token, 
			token.nextToken
		);
		// add token flags 
		newToken.addFlag(TokenFlag.BEGINS_LINE);
		newToken.addFlag(TokenFlag.ENDS_LINE);
		
		changeRowUntilEOF(token.nextToken, 1);// TODO create constant
		
		plugTokenBehind(token, newToken);
	}

	/**
	 * Plugs new token to a list behind token.
	 * @param token
	 * @param newNext
	 */
	private static void plugTokenBehind(Token token, Token newNext) {
		
		token.nextToken.prevToken = newNext;
		token.nextToken = newNext;
	}
	
	/**
	 * Odsadi radek zacinajici tokenem <code>start</code> na uroven 
	 * <code>level</code>.
	 * 
	 * @param startToken prvni token na radce
	 * @param indentationLevel uroven, na kterou ma byt tato radka odsazena
	 * @return token, kterym radka zacina po odsazeni (neni nutne stejny jako 
	 * <code>start</code>)
	 */
	private static Token indentLine(Token startToken, int indentationLevel) {
		 // TODO very long method
		Token result = startToken; // TODO check if result is necessary

		if (startToken.isType(TokenType.WHITESPACE)) {

			boolean isEndLineToken = startToken
				.containsFlag(TokenFlag.ENDS_LINE);
			
			int tokenTextLength = startToken.text.length();
			boolean isDiffIndentation = tokenTextLength != indentationLevel;
			
			if (!isEndLineToken && isDiffIndentation) {
				
				int delta = 0;
				if (indentationLevel > 0) {
					delta = indentationLevel - tokenTextLength;

					// replace text with correct value
					startToken.text = StringUtils
						.repeat(SPACE, indentationLevel);
					
					changeColUntilEOL(startToken.nextToken, delta);
				} else {

					removeTokenFromLinkedList(startToken);					
					
					startToken.nextToken.addFlag(TokenFlag.BEGINS_LINE);
					
					// shift columns about removed token back
					/*delta = -start.col;*/
					delta = -1 * tokenTextLength;
					changeColUntilEOL(startToken.nextToken, delta);
					
					result = startToken.nextToken;
				}
			}
		} else if (indentationLevel > 0) {
			
			String text = StringUtils.repeat(SPACE, indentationLevel);

			Token newToken = new Token(
				text, 
				TokenType.WHITESPACE,
				startToken.getFlags(),
				startToken.row, 
				startToken.column, 
				startToken.prevToken, 
				startToken
			);
			newToken.removeFlag(TokenFlag.ENDS_LINE);
			newToken.addFlag(TokenFlag.BEGINS_LINE);
			
			changeColUntilEOL(startToken, indentationLevel);
			
			insertTokenBefore(newToken, startToken);
			startToken.removeFlag(TokenFlag.BEGINS_LINE);
		} else {
			// TODO not nice
			/* nic nedelej, uz mame hotovo */
		}

		return result;
	}
	
	private static void insertTokenBefore(Token insertedToken, Token token) {
		
		if (token.prevToken != null) { 
			token.prevToken.nextToken = insertedToken;
		}
		token.prevToken = insertedToken;		
	}
	
	private static void removeTokenFromLinkedList(Token token) {
		
		if (token.prevToken != null) { 
			token.prevToken.nextToken = token.nextToken;
		}
		
		if (token.nextToken != null) { 
			token.nextToken.prevToken = token.prevToken;
		}
	}
	
	/** 
	 * Odsadi urcite typy komentaru podle toho, k cemu patri.
	 * 
	 * @param tokenList spojovy seznam tokenu, kde se maji najit a odsadit 
	 * komentare
	 */
	static void indentComments(Token tokenList) {

		for (Token token = tokenList; token != null; token = token.nextToken) {
			if (token.isType(TokenType.COMMENT)) {

				if (!isTokenStandalone(token)) {
					continue;
				}

				/* Ted najdeme neco, k cemu by se mohl tento 
				 * komentar vztahovat. */
				Token tokenBelongComment = skipWhitespaceCommentToken(
					token.nextToken
				);
				
				if (isSpecialReservedWord(tokenBelongComment)) {
					continue;
				}				
				
				Token firstToken = getFirstLineToken(token); 
				indentLine(firstToken, tokenBelongComment.column);
			}
		}
	}  
	
	private static boolean isSpecialReservedWord(Token token) {

		boolean isReservedEnd = token.equalsTokenTypeAndText(
			TokenType.RESERVED_WORD, 
			STRING_END
		);
		
		boolean isReservedUntil = token.equalsTokenTypeAndText(
			TokenType.RESERVED_WORD, 
			STRING_UNTIL
		);
		
		return (token == null || isReservedEnd || isReservedUntil);	
	}
	
	private static Token getFirstLineToken(Token startToken) {

		for (
			Token token = startToken;
			token != null;
			token = token.prevToken
		) {
			if (token.containsFlag(TokenFlag.BEGINS_LINE)) {
				return token;
			}
		}
		
		return startToken;
	}
	
	private static boolean isTokenStandalone(Token startToken) {

		for (
			Token token = startToken; 
			token != null; 
			token = token.prevToken
		) {
			if (!token.type.equals(TokenType.WHITESPACE)) {
				return false;
			}

			if (token.containsFlag(TokenFlag.BEGINS_LINE)) {
				return true;
			}
		}
		
		return true;
	}
		
	public interface ISkipCondition {
		boolean condition(Token token);
	}
	
	public static class TokenConditionIterator implements Iterable<Token> {

		private Token token;
		private ISkipCondition tokenCondition;
		
		public TokenConditionIterator(
			Token token, 
			ISkipCondition tokenCondition
		) {
			this.token = token;
			this.tokenCondition = tokenCondition;
		}
		
		@Override
		public Iterator<Token> iterator() {
			return new Iterator<Token>() {

				@Override
				public boolean hasNext() {
					return token != null && tokenCondition.condition(token);
				}

				@Override
				public Token next() {
					
					if (!hasNext()) {
		                throw new NoSuchElementException();
					}
					
					Token result = token;
					token = token.nextToken;
					
					return result;
				}

				@Override
				public void remove() {
					throw new IllegalStateException("Not implemented.");
				}
			};
		}
	}
}

/** Objekt tokenu. */
class Token implements Cloneable {
	
	public enum TokenType {
		VIRTUAL_ROUND_BRACKET,
		WHITESPACE,
		COMMENT,
		RESERVED_WORD;
	}
	
	/* Vlajky tokenu. TF = Token Flag. */
	public enum TokenFlag {

		/** Nic. */
		NONE(0),

		/** Zacina radek */
		BEGINS_LINE(1 << 0), // 1

		/** ukoncuje radek */
		ENDS_LINE(1 << 1), // 2

		/** povoleny upravy casu rezervovanych slov */
		EDIT_TIME_RESERVED_WORDS_ENABLED(1 << 2), // 4

		/** povoleny upravy casu direktiv */
		EDIT_TIME_DIRECTIVES_ENABLED(1 << 3), // 8

		/** povoleny upravy casu identifikatoru */
		EDIT_TIME_IDENTIFIERS_ENABLED(1 << 4), // 16

		/** povoleno vkladani mezer na vhodna mista */
		INSERT_SPACES_ENABLED(1 << 5), // 32

		/** povoleno odsazovani */
		INDENT_ENABLED(1 << 6), // 64

		/** povoleno zalamovani radku */
		LINE_WRAP_ENABLED(1 << 7), // 128

		/** povoleno vkladani prazdnych radku */
		INSERT_BLANK_LINES_ENABLED(1 << 8); // 256

		private final int value;

		private TokenFlag(int value) {
			this.value = value;
		}
		
		public int getValue() {
			return value;
		}
	}

	/** Text. */
	public String text;
	
	/** Trida. */
	public TokenType type;
	
	/** Vlajky. */
	public int flags;
	
	/** Radek, kde zacina (indexovano od 0). */
	public int row;
	
	/** Sloupec, kde zacina (indexovano od 0). */
	public int column;
	
	/** Odkaz na predchozi token ve spojaku. */
	public Token prevToken;
	
	/** Odkaz na nasledujici token ve spojaku. */
	public Token nextToken;

	/**
	 * Vytvori token podle zadanych parametru.
	 * 
	 * @param text text
	 * @param type trida
	 * @param flags vlajky
	 * @param row radek, kde zacina (indexovano od 0)
	 * @param column sloupec, kde zacina (indexovano od 0)
	 * @param prevToken odkaz na predchozi token ve spojaku
	 * @param nextToken odkaz na nasledujici token ve spojaku
	 */
	public Token(
		String text, 
		TokenType type, 
		int flags, 
		int row, 
		int column, 
		Token prevToken, 
		Token nextToken
	) {	
		this.text = text;
		this.type = type;
		this.flags = flags;	
		this.row = row;
		this.column = column;
		this.prevToken = prevToken;
		this.nextToken = nextToken;  
	}
	
	/**
	 * Odpovida token dane tride a textu?
	 * 
	 * @param aClass trida
	 * @param aText text
	 * @return <code>true</code> pokud token odpovida dane tride a textu;
	 *         <code>false</code> pokud token neodpovida dane tride a textu 
	 */  
	boolean equalsTokenTypeAndText(TokenType tokenType, String aText) {
		return isType(tokenType) && checkText(aText); 
	}
	
	/**
	 * Returns true if instance text equals to investigate text.
	 */
	private boolean checkText(String text) {
		return this.text.equalsIgnoreCase(text);
	}

	public void addFlag(TokenFlag tokenFlag) {
		flags |= tokenFlag.value;
	}

	public void removeFlag(TokenFlag tokenFlag) {
		flags &= ~tokenFlag.value;
	}
	
	public boolean containsFlag(TokenFlag requiredFlag) {
		int result = flags & requiredFlag.getValue();
		return result == requiredFlag.getValue();
	}

	public int getFlags() {
		return flags;
	}
	
	/**
	 * Vytvori klon objektu.
	 * @return klon objektu
	 */
	@Override
	public Object clone() {
		Token nextToken = null;
		Token prevToken = null;
		return new Token(text, type, flags, row, column, prevToken, nextToken);
	}

	
	public boolean isType(TokenType type) {
		return this.type.equals(type);
	}
	
	
	/** Trida popisujici jednu uroven odsazeni. Slouzi jako jedna polozka v 
	 * zasobniku odsazeni. */
	private static final class Indentation {

		/** Druh odsazeni. */ 
		public TokenType type;
		/** Ukazatel na dalsi polozku zasobniku, */ 
		public Indentation next;       

		Indentation(TokenType tokenType)	{
			this.type = tokenType;
		}
		
		public boolean isVirtual() {
			return TokenType.VIRTUAL_ROUND_BRACKET.equals(type);
		}
		
		public boolean isType(TokenType type) {
			return type.equals(type);
		}
	}

	/** Trida popisujici aktualni stav odsazovani v prubehu algoritmu. */
	private final static class IndentContext {
		
		/**	Indentation size */
		public static final int INDENTATOIN_SIZE = 4;
		
		/** Zasobnik <code>Indent</code>u. */
		public Stack<Indentation> indentationStack = new Stack<>();
		
		/** Aktualni uroven odsazeni. */     
		public int currentLevel;
		
		/** Minimalni uroven odsazeni na teto radce. */
		public int minLevel;   

		/**
		 * Odsadi o jednu uroven, jejiz druh je urcen parametrem 
		 * <code>klass</code>.
		 * 
		 * @param indentationType druh odsazeni
		 */
		public void indent(TokenType indentationType) {
			
			Indentation newIndentation = new Indentation(indentationType);
			indentationStack.push(newIndentation);
			
			if (!newIndentation.isVirtual()) {
				currentLevel += INDENTATOIN_SIZE;
			}
		}

		/** Odsadi zpatky. */
		public void unindent() {
			
			Indentation lastIndentation = null;
			
			try {
				lastIndentation = indentationStack.pop();	
			} catch (EmptyStackException e) {
				return;
			}
			
			if (lastIndentation == null) {
				return;
			}
			
			if (!lastIndentation.isVirtual()) {
				currentLevel -= INDENTATOIN_SIZE;
			}

			if (minLevel > currentLevel) {
				minLevel = currentLevel;
			}
		}
		
		/** Odsadi zpatky, ale nesnizi pritom <code>minLevel</code>. */
		public void unindentNotReduceMinimalLevel() {
			
			Indentation lastIndentation = null;
			try {
				lastIndentation = indentationStack.pop();
			} catch (EmptyStackException e) {
				return;
			} 
			
			if (lastIndentation != null && !lastIndentation.isVirtual()) {
				currentLevel -=INDENTATOIN_SIZE;
			}
		}

		/**
		 * Zjistuje, zda je druh odsazeni na vrcholu zasobniku 
		 * <code>klass</code>.
		 * 
		 * @param type druh odsazeni, se kterym se porovnava druh odsazeni na 
		 * vrcholu zasobniku
		 * @return <code>true</code> pokud je zasobnik neprazdny a druh 
		 * odsazeni na jeho vrcholu je <code>klass</code>;
		 * <code>false</code> pokud je zasobnik prazdny nebo je druh odsazeni
		 * na jeho vrcholu jiny nez <code>klass</code>
		 */
		public boolean isTopType(TokenType type) {
			
			Indentation lastIndentation = null;
			try {
				lastIndentation = indentationStack.peek();
			} catch (EmptyStackException  e) {
				return false;
			}

			return lastIndentation.isType(type);
		}
	};

}

// TODO should be public
class StringUtils {
	
	/**
	 * Returns a string consisting of a specific number of concatenated 
	 * copies of an input string. For example, repeat("hey", 3) 
	 * returns the string "heyheyhey".
	 * 
	 * @param string - any non-null string
	 * @param count - the number of times to repeat it; a nonnegative integer 
	 * @returna string containing string repeated count times (the empty string 
	 * if count is zero) 
	 */
	public static String repeat(String string, int count) {
		
		if (count < 0) {
			
			throw new IllegalArgumentException("Count is negative.");
		} else {
			// create a string made up of count copies of string
			return String.format(String.format("%%0%dd", count), 0)
				.replace("0", string);
		}
	}
}
